package sk.stuba.fiit.perconik.activity.listeners.java.dom; import java.util.Iterator; import java.util.List; import com.google.common.base.Function; import org.eclipse.jdt.core.dom.ASTMatcher; import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.AbstractTypeDeclaration; import org.eclipse.jdt.core.dom.AnnotationTypeDeclaration; import org.eclipse.jdt.core.dom.AnnotationTypeMemberDeclaration; import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.jdt.core.dom.EnumConstantDeclaration; import org.eclipse.jdt.core.dom.EnumDeclaration; import org.eclipse.jdt.core.dom.FieldDeclaration; import org.eclipse.jdt.core.dom.Initializer; import org.eclipse.jdt.core.dom.MethodDeclaration; import org.eclipse.jdt.core.dom.TypeDeclaration; import org.eclipse.jdt.core.dom.VariableDeclarationFragment; import difflib.Delta; import difflib.DiffUtils; import difflib.Patch; import sk.stuba.fiit.perconik.core.java.dom.MatchingNode; import sk.stuba.fiit.perconik.core.java.dom.difference.NodeDeltaSet; import sk.stuba.fiit.perconik.core.java.dom.difference.NodeDifferencer; import sk.stuba.fiit.perconik.eclipse.jdt.core.dom.NodeType; import sk.stuba.fiit.perconik.eclipse.jdt.core.dom.TreeApiLevel; import static java.lang.String.format; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.collect.Lists.newLinkedList; import static sk.stuba.fiit.perconik.core.java.dom.MatchingNode.unwrap; import static sk.stuba.fiit.perconik.core.java.dom.MatchingNode.wrap; // TODO extract abstract class as public API, whole isSimilar functionality can be in an ASTMatcher final class CompilationUnitDifferencer<N extends ASTNode> implements NodeDifferencer<CompilationUnit, N> { private static final ASTMatcher matcher = new ASTMatcher(true); private final Function<? super CompilationUnit, ? extends Iterable<? extends N>> collector; private NodeDeltaSet.Builder<N> builder; CompilationUnitDifferencer(final Function<? super CompilationUnit, ? extends Iterable<? extends N>> collector) { this.collector = checkNotNull(collector); } public NodeDeltaSet<N> difference(final CompilationUnit original, final CompilationUnit revised) { this.builder = NodeDeltaSet.builder(); if (original != null || revised != null) { Iterable<? extends N> originalNodes = this.collector.apply(original); Iterable<? extends N> revisedNodes = this.collector.apply(revised); this.compute(originalNodes, revisedNodes); } return this.builder.build(); } private void compute(final Iterable<? extends N> original, final Iterable<? extends N> revised) { final Patch<MatchingNode<N>> patch = DiffUtils.diff(wrap(original), wrap(revised)); for (final Delta<MatchingNode<N>> delta: patch.getDeltas()) { switch (delta.getType()) { case DELETE: for (N node: unwrap(delta.getOriginal().getLines())) { this.builder.delete(node); } break; case INSERT: for (N node: unwrap(delta.getRevised().getLines())) { this.builder.add(node); } break; case CHANGE: List<N> originalNodes = unwrap(delta.getOriginal().getLines()); List<N> revisedNodes = unwrap(delta.getRevised().getLines()); List<N> unmatchedOriginalNodes = newLinkedList(); main: for (N originalNode: originalNodes) { int revisedNodesSize = revisedNodes.size(); for (int k = 0; k < revisedNodesSize; k ++) { N revisedNode = revisedNodes.get(k); if (isSimilar(originalNode, revisedNode)) { this.builder.modify(originalNode, revisedNode); revisedNodes.remove(k); continue main; } } unmatchedOriginalNodes.add(originalNode); } for (N originalNode: unmatchedOriginalNodes) { this.builder.delete(originalNode); } for (N revisedNode: revisedNodes) { this.builder.add(revisedNode); } break; default: throw new IllegalStateException(format("Unknown delta type %s", delta.getType())); } } } // body declaration routers private static boolean similar(final List<ASTNode> original, final List<ASTNode> revised) { if (original.size() != revised.size()) { return false; } Iterator<ASTNode> revisedIterator = revised.iterator(); for (ASTNode originalNode: original) { if (!isSimilar(originalNode, revisedIterator.next())) { return false; } } return true; } private static boolean isSimilar(final ASTNode original, final ASTNode revised) { if (original instanceof AbstractTypeDeclaration) { return isSimilar((AbstractTypeDeclaration) original, revised); } switch (NodeType.valueOf(original)) { case ANNOTATION_TYPE_MEMBER_DECLARATION: return isSimilar((AnnotationTypeMemberDeclaration) original, revised); case ENUM_CONSTANT_DECLARATION: return isSimilar((EnumConstantDeclaration) original, revised); case FIELD_DECLARATION: return isSimilar((FieldDeclaration) original, revised); case INITIALIZER: return isSimilar((Initializer) original, revised); case METHOD_DECLARATION: return isSimilar((MethodDeclaration) original, revised); default: return false; } } private static boolean isSimilar(final AbstractTypeDeclaration original, final ASTNode revised) { switch (NodeType.valueOf(original)) { case ANNOTATION_TYPE_DECLARATION: return isSimilar((AnnotationTypeDeclaration) original, revised); case ENUM_DECLARATION: return isSimilar((EnumDeclaration) original, revised); case TYPE_DECLARATION: return isSimilar((TypeDeclaration) original, revised); default: return false; } } // abstract type declarations private static boolean isSimilar(final AnnotationTypeDeclaration original, final ASTNode revised) { if (!(revised instanceof AnnotationTypeDeclaration)) { return false; } AnnotationTypeDeclaration other = (AnnotationTypeDeclaration) revised; boolean nameMatches = matcher.safeSubtreeMatch(original.getName(), other.getName()); if (nameMatches) { return true; } boolean restMatches = matcher.safeSubtreeMatch(original.getJavadoc(), other.getJavadoc()) && matcher.safeSubtreeListMatch(original.modifiers(), other.modifiers()) && similar(original.bodyDeclarations(), other.bodyDeclarations()); return restMatches; } private static boolean isSimilar(final EnumDeclaration original, final ASTNode revised) { if (!(revised instanceof EnumDeclaration)) { return false; } EnumDeclaration other = (EnumDeclaration) revised; boolean nameMatches = matcher.safeSubtreeMatch(original.getName(), other.getName()); if (nameMatches) { return true; } boolean restMatches = matcher.safeSubtreeMatch(original.getJavadoc(), other.getJavadoc()) && matcher.safeSubtreeListMatch(original.modifiers(), other.modifiers()) && matcher.safeSubtreeListMatch(original.superInterfaceTypes(), other.superInterfaceTypes()) && matcher.safeSubtreeListMatch(original.enumConstants(), other.enumConstants()) && similar(original.bodyDeclarations(), other.bodyDeclarations()); return restMatches; } private static boolean isSimilar(final TypeDeclaration original, final ASTNode revised) { if (!(revised instanceof TypeDeclaration)) { return false; } TypeDeclaration other = (TypeDeclaration) revised; boolean nameMatches = matcher.safeSubtreeMatch(original.getName(), other.getName()); if (nameMatches) { return true; } boolean restMatches = true; switch (TreeApiLevel.valueOf(original)) { case JLS3: case JLS4: restMatches = restMatches && matcher.safeSubtreeListMatch(original.modifiers(), other.modifiers()) && matcher.safeSubtreeListMatch(original.typeParameters(), other.typeParameters()) && matcher.safeSubtreeMatch(original.getSuperclassType(), other.getSuperclassType()) && matcher.safeSubtreeListMatch(original.superInterfaceTypes(), other.superInterfaceTypes()); break; default: throw new UnsupportedOperationException(format("Unsupported tree API level %s", TreeApiLevel.valueOf(original))); } restMatches = restMatches && original.isInterface() == other.isInterface() && matcher.safeSubtreeMatch(original.getJavadoc(), other.getJavadoc()) && matcher.safeSubtreeMatch(original.getName(), other.getName()) && similar(original.bodyDeclarations(), other.bodyDeclarations()); return restMatches; } // body declarations except abstract type declarations private static boolean isSimilar(final AnnotationTypeMemberDeclaration original, final ASTNode revised) { if (!(revised instanceof AnnotationTypeMemberDeclaration)) { return false; } AnnotationTypeMemberDeclaration other = (AnnotationTypeMemberDeclaration) revised; boolean nameMatches = matcher.safeSubtreeMatch(original.getName(), other.getName()); if (nameMatches) { return true; } boolean restMatches = matcher.safeSubtreeMatch(original.getJavadoc(), other.getJavadoc()) && matcher.safeSubtreeListMatch(original.modifiers(), other.modifiers()) && matcher.safeSubtreeMatch(original.getType(), other.getType()) && matcher.safeSubtreeMatch(original.getDefault(), other.getDefault()); return restMatches; } private static boolean isSimilar(final EnumConstantDeclaration original, final ASTNode revised) { if (!(revised instanceof EnumConstantDeclaration)) { return false; } EnumConstantDeclaration other = (EnumConstantDeclaration) revised; boolean nameMatches = matcher.safeSubtreeMatch(original.getName(), other.getName()); if (nameMatches) { return true; } boolean restMatches = matcher.safeSubtreeMatch(original.getJavadoc(), other.getJavadoc()) && matcher.safeSubtreeListMatch(original.modifiers(), other.modifiers()) && matcher.safeSubtreeListMatch(original.arguments(), other.arguments()) && matcher.safeSubtreeMatch(original.getAnonymousClassDeclaration(), other.getAnonymousClassDeclaration()); return restMatches; } private static boolean isSimilar(final FieldDeclaration original, final ASTNode revised) { if (!(revised instanceof FieldDeclaration)) { return false; } FieldDeclaration other = (FieldDeclaration) revised; boolean fragmentMatches = false; for (Object originalFragment: original.fragments()) { if (!(originalFragment instanceof VariableDeclarationFragment)) { continue; } for (Object otherFragment: other.fragments()) { if (!(otherFragment instanceof ASTNode)) { continue; } if (isSimilar((VariableDeclarationFragment) originalFragment, (ASTNode) otherFragment)) { fragmentMatches = true; break; } } } if (fragmentMatches) { return true; } boolean restMatches = true; switch (TreeApiLevel.valueOf(original)) { case JLS3: case JLS4: restMatches = restMatches && matcher.safeSubtreeListMatch(original.modifiers(), other.modifiers()); break; default: throw new UnsupportedOperationException(format("Unsupported tree API level %s", TreeApiLevel.valueOf(original))); } restMatches = restMatches && matcher.safeSubtreeMatch(original.getJavadoc(), other.getJavadoc()) && matcher.safeSubtreeMatch(original.getType(), other.getType()); return restMatches; } @SuppressWarnings("unused") private static boolean isSimilar(final Initializer original, final ASTNode revised) { if (!(revised instanceof Initializer)) { return false; } return true; } @SuppressWarnings("deprecation") private static boolean isSimilar(final MethodDeclaration original, final ASTNode revised) { if (!(revised instanceof MethodDeclaration)) { return false; } MethodDeclaration other = (MethodDeclaration) revised; boolean nameMatches = matcher.safeSubtreeMatch(original.getName(), other.getName()); if (nameMatches) { return true; } boolean restMatches = true; switch (TreeApiLevel.valueOf(original)) { case JLS3: case JLS4: restMatches = restMatches && matcher.safeSubtreeListMatch(original.modifiers(), other.modifiers()) && matcher.safeSubtreeMatch(original.getReturnType2(), other.getReturnType2()) && matcher.safeSubtreeListMatch(original.typeParameters(), other.typeParameters()); break; default: throw new UnsupportedOperationException(format("Unsupported tree API level %s", TreeApiLevel.valueOf(original))); } restMatches = restMatches && original.isConstructor() == other.isConstructor() && matcher.safeSubtreeMatch(original.getJavadoc(), other.getJavadoc()) && matcher.safeSubtreeListMatch(original.parameters(), other.parameters()) && original.getExtraDimensions() == other.getExtraDimensions() && matcher.safeSubtreeListMatch(original.thrownExceptions(), other.thrownExceptions()) && matcher.safeSubtreeMatch(original.getBody(), other.getBody()); return restMatches; } // body declaration helpers private static boolean isSimilar(final VariableDeclarationFragment original, final ASTNode revised) { if (!(revised instanceof VariableDeclarationFragment)) { return false; } VariableDeclarationFragment other = (VariableDeclarationFragment) revised; boolean nameMatches = matcher.safeSubtreeMatch(original.getName(), other.getName()); if (nameMatches) { return true; } boolean restMatches = original.getExtraDimensions() == other.getExtraDimensions() && matcher.safeSubtreeMatch(original.getInitializer(), other.getInitializer()); return restMatches; } }